
# 导入日志模块，用于记录程序运行信息
import logging
# 导入 uuid 模块，用于生成唯一标识符
import uuid
# 从 qtpy 库导入 QtCore 模块，提供核心的非 GUI 功能
from qtpy import QtCore
# 从 qtpy 库导入 QtGui 模块，提供图形用户界面相关的功能
from qtpy import QtGui
# 从 qtpy 库导入 QtSvg 模块，用于处理 SVG 图形，不过此导入未使用，后续可移除
from qtpy import QtSvg
# 从 qtpy.QtWidgets 导入所有类，用于创建 GUI 界面
from qtpy.QtWidgets import *
# 从 qtpy.QtSvgWidgets 导入 QGraphicsSvgItem 类，用于在图形场景中显示 SVG 图形
from qtpy.QtSvgWidgets import QGraphicsSvgItem

# 从 src 库的 ConfigManager 模块导入 ConfigManager 类，用于管理配置信息
from src.ConfigManager import ConfigManager
# 从 src 库的 Core.Common 模块导入所有内容，不过此导入未使用，后续可移除
from src.Core.Common import *
# 从 src 库的 UI.Canvas.UIPinBase 模块导入相关类，其中 UIPinBase 未使用，后续可移除
from src.UI.Canvas.UIPinBase import UIPinBase, getUIPinInstance, PinGroup
# 从 src 库的 UI.EditorHistory 模块导入 EditorHistory 类，用于管理编辑器的历史记录
from src.UI.EditorHistory import EditorHistory
# 从 src 库的 UI.Canvas.UICommon 模块导入所有内容，用于提供 UI 通用功能
from src.UI.Canvas.UICommon import *
# 从 src 库的 UI.Widgets.InputWidgets 模块导入 createInputWidget 函数，用于创建输入小部件
from src.UI.Widgets.InputWidgets import createInputWidget
# 从 src 库的 UI.Canvas.Painters 模块导入 NodePainter 类，用于绘制节点
from src.UI.Canvas.Painters import NodePainter
# 从 src 库的 UI.Widgets.PropertiesFramework 模块导入 CollapsibleFormWidget 类，用于创建可折叠的表单小部件
from src.UI.Widgets.PropertiesFramework import CollapsibleFormWidget
# 从 src 库的 UI.UIInterfaces 模块导入 IPropertiesViewSupport 接口，用于支持属性视图
from src.UI.UIInterfaces import IPropertiesViewSupport
# 从 src 库的 UI.UIInterfaces 模块导入 IUINode 接口，用于定义 UI 节点的接口
from src.UI.UIInterfaces import IUINode
# 从 src 库的 UI.Canvas.NodeActionButton 模块导入 NodeActionButtonBase 类，作为节点动作按钮的基类
from src.UI.Canvas.NodeActionButton import NodeActionButtonBase
# 从 src 库的 UI.Utils.stylesheet 模块导入 Colors 类，用于管理颜色样式
from src.UI.Utils.stylesheet import Colors

# 从 collections 模块导入 OrderedDict 类，用于创建有序字典
from collections import OrderedDict

# 全局字典，用于存储 UI 节点的工厂函数
UI_NODES_FACTORIES = {}


class CollapseNodeActionButton(NodeActionButtonBase):
    """
    用于折叠和展开节点的动作按钮类。

    继承自 NodeActionButtonBase，处理节点折叠和展开时的图标切换。
    """

    def __init__(self, svgFilePath, action, uiNode):
        """
        初始化 CollapseNodeActionButton 实例。

        :param svgFilePath: SVG 图标文件的路径。
        :param action: 关联的动作。
        :param uiNode: 关联的 UI 节点。
        """
        super(CollapseNodeActionButton, self).__init__(svgFilePath, action, uiNode)
        # 设置 SVG 图标的元素 ID 为 "Collapse"
        self.svgIcon.setElementId("Collapse")

    def mousePressEvent(self, event):
        """
        处理鼠标按下事件，根据节点的折叠状态切换 SVG 图标的元素 ID。

        :param event: 鼠标按下事件对象。
        """
        super(CollapseNodeActionButton, self).mousePressEvent(event)
        if self.parentItem().collapsed:
            # 若节点已折叠，切换图标为展开状态
            self.svgIcon.setElementId("Expand")
        else:
            # 若节点未折叠，切换图标为折叠状态
            self.svgIcon.setElementId("Collapse")


class NodeNameValidator(QtGui.QRegularExpressionValidator):
    """
    用于验证节点名称的正则表达式验证器类。

    继承自 QtGui.QRegularExpressionValidator，确保节点名称符合特定的正则表达式规则。
    """

    def __init__(self, parent=None):
        """
        初始化 NodeNameValidator 实例。

        :param parent: 父对象，默认为 None。
        """
        super(NodeNameValidator, self).__init__(
            QtCore.QRegularExpression("^[a-zA-Z][a-zA-Z0-9_]*$"), parent
        )


class InputTextField(QGraphicsTextItem):
    """
    用于输入文本的图形文本项类。

    提供文本编辑功能，支持验证输入的文本，并发出编辑开始和结束的信号。
    """
    # 定义编辑完成信号，参数为布尔值，表示编辑是否被接受
    editingFinished = QtCore.Signal(bool)
    # 定义开始编辑信号
    startEditing = QtCore.Signal()

    def __init__(self, text, node, parent=None, singleLine=False, validator=None):
        """
        初始化 InputTextField 实例。

        :param text: 初始文本。
        :param node: 关联的节点。
        :param parent: 父对象，默认为 None。
        :param singleLine: 是否为单行文本，默认为 False。
        :param validator: 文本验证器，默认为 None。
        """
        super(InputTextField, self).__init__(text, parent)
        self.node = node
        # 设置图形项的标志，包括发送几何变化通知和可选择
        self.setFlags(
            QGraphicsWidget.ItemSendsGeometryChanges | QGraphicsWidget.ItemIsSelectable
        )
        self.singleLine = singleLine
        # 设置对象名称
        self.setObjectName("Nothing")
        # 保存原始的鼠标移动事件处理方法
        self.origMoveEvent = self.mouseMoveEvent
        # 将鼠标移动事件处理方法替换为节点的鼠标移动事件处理方法
        self.mouseMoveEvent = self.node.mouseMoveEvent
        self.validator = validator
        # 保存编辑前的文本
        self.textBeforeEditing = ""

    def keyPressEvent(self, event):
        """
        处理键盘按键事件，根据验证器和按键类型处理文本输入。

        :param event: 键盘按键事件对象。
        """
        # 获取当前按下的按键
        currentKey = event.key()
        if self.validator is not None:
            # 获取按键对应的文本
            keyButtonText = event.text()
            # 创建一个新的文本文档，内容为当前文本
            doc = QtGui.QTextDocument(self.document().toPlainText())
            # 获取当前选中的文本
            selectedText = self.textCursor().selectedText()
            # 查找选中的文本在文档中的位置
            cursor = doc.find(selectedText)
            # 在光标位置插入新的文本
            cursor.insertText(keyButtonText)
            # 获取未来的文本内容
            futureText = doc.toPlainText()
            # 使用验证器验证未来的文本内容
            validatorState, chunk, pos = self.validator.validate(futureText, 0)
            if currentKey not in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
                if validatorState == QtGui.QValidator.Invalid:
                    # 若验证不通过，忽略此次输入
                    return

        if currentKey == QtCore.Qt.Key_Escape:
            # 若按下 ESC 键，恢复编辑前的文本并清除焦点
            self.setPlainText(self.textBeforeEditing)
            self.clearFocus()
            super(InputTextField, self).keyPressEvent(event)
            return

        if self.singleLine:
            if currentKey in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
                if self.toPlainText() == "":
                    # 若输入为空，恢复编辑前的文本并发出编辑未接受信号
                    self.setPlainText(self.textBeforeEditing)
                    event.ignore()
                    self.editingFinished.emit(False)
                    self.clearFocus()
                else:
                    # 若输入不为空，忽略事件并清除焦点
                    event.ignore()
                    # self.editingFinished.emit(True)
                    self.clearFocus()
            else:
                super(InputTextField, self).keyPressEvent(event)
        else:
            super(InputTextField, self).keyPressEvent(event)

    def mousePressEvent(self, event):
        """
        处理鼠标按下事件，根据对象名称决定是调用自身还是节点的鼠标按下事件处理方法。

        :param event: 鼠标按下事件对象。
        """
        if self.objectName() == "MouseLocked":
            super(InputTextField, self).mousePressEvent(event)
        else:
            self.node.mousePressEvent(event)
            self.clearFocus()

    def mouseReleaseEvent(self, event):
        """
        处理鼠标释放事件，根据对象名称决定是调用自身还是节点的鼠标释放事件处理方法。

        :param event: 鼠标释放事件对象。
        """
        if self.objectName() == "MouseLocked":
            super(InputTextField, self).mouseReleaseEvent(event)
        else:
            self.node.mouseReleaseEvent(event)
            self.clearFocus()

    def mouseDoubleClickEvent(self, event):
        """
        处理鼠标双击事件，设置图形项可获得焦点，发出开始编辑信号并获取焦点。

        :param event: 鼠标双击事件对象。
        """
        super(InputTextField, self).mouseDoubleClickEvent(event)
        # 设置图形项可获得焦点
        self.setFlag(QGraphicsWidget.ItemIsFocusable, True)
        # 发出开始编辑信号
        self.startEditing.emit()
        # 获取焦点
        self.setFocus()

    def focusInEvent(self, event):
        """
        处理获得焦点事件，禁用节点画布的快捷键，设置文本交互标志，保存编辑前的文本。

        :param event: 焦点进入事件对象。
        """
        self.node.canvasRef().disableShortcuts()
        # 设置文本交互标志为文本编辑器交互模式
        self.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction)
        self.setObjectName("MouseLocked")
        self.textBeforeEditing = self.toPlainText()
        # 恢复原始的鼠标移动事件处理方法
        self.mouseMoveEvent = self.origMoveEvent
        super(InputTextField, self).focusInEvent(event)

    def focusOutEvent(self, event):
        """
        处理失去焦点事件，启用节点画布的快捷键，清除文本选择，设置文本交互标志，发出编辑完成信号。

        :param event: 焦点离开事件对象。
        """
        self.node.canvasRef().enableShortcuts()
        # 获取文本光标
        cursor = self.textCursor()
        # 清除文本选择
        cursor.clearSelection()
        self.setTextCursor(cursor)
        super(InputTextField, self).focusOutEvent(event)
        # 设置文本交互标志为无文本交互模式
        self.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
        # 设置图形项不可获得焦点
        self.setFlag(QGraphicsWidget.ItemIsFocusable, False)
        self.setObjectName("Nothing")
        if self.toPlainText() == "" and self.validator is not None:
            # 若输入为空，恢复编辑前的文本并发出编辑未接受信号
            self.setPlainText(self.textBeforeEditing)
            self.editingFinished.emit(False)
        else:
            # 若输入不为空，发出编辑接受信号
            self.editingFinished.emit(True)
        # 将鼠标移动事件处理方法替换为节点的鼠标移动事件处理方法
        self.mouseMoveEvent = self.node.mouseMoveEvent

    def setGeometry(self, rect):
        """
        设置图形项的几何形状，准备几何变化并设置位置。

        :param rect: 矩形区域，用于设置图形项的位置和大小。
        """
        self.prepareGeometryChange()
        self.setPos(rect.topLeft())


class NodeName(QGraphicsWidget):
    """
    表示节点名称的图形小部件类。

    包含一个输入文本框，用于显示和编辑节点名称，并处理相关的鼠标和悬停事件。
    """

    def __init__(self, parent=None):
        """
        初始化 NodeName 实例。

        :param parent: 父对象，默认为 None。
        """
        super(NodeName, self).__init__(parent)
        # 设置接受悬停事件
        self.setAcceptHoverEvents(True)
        # 设置图形项的标志，包括发送几何变化通知
        self.setFlag(QGraphicsWidget.ItemSendsGeometryChanges)
        # 设置缓存模式为设备坐标缓存
        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
        # 创建一个输入文本框，用于显示和编辑节点名称
        self.labelItem = InputTextField(
            self.parentItem().getName(),
            parent,
            self,
            singleLine=True,
            validator=NodeNameValidator(),
        )
        # 设置输入文本框的默认文本颜色为父节点的标签文本颜色
        self.labelItem.setDefaultTextColor(self.parentItem()._labelTextColor)
        self.labelItem.setAcceptHoverEvents(True)
        # 连接文本内容变化信号到父节点的更新节点形状方法
        self.labelItem.document().contentsChanged.connect(
            self.parentItem().updateNodeShape
        )
        # 连接编辑完成信号到父节点的完成重命名方法
        self.labelItem.editingFinished.connect(self.parentItem().finalizeRename)

        self.labelItem.hoverMoveEvent = self.hoverMoveEvent
        # 创建一个字体对象，设置字体为 Consolas，字号为 6
        self._font = QtGui.QFont("Consolas")
        self._font.setPointSize(6)
        self.labelItem.setFont(self._font)
        # 设置图形项为输入文本框
        self.setGraphicsItem(self.labelItem)
        # 标记是否悬停
        self.hovered = False
        # 设置大小策略为最大
        self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)

    def getFont(self):
        """
        获取输入文本框的字体。

        :return: 字体对象。
        """
        return self.labelItem.font()

    def getPlainText(self):
        """
        获取输入文本框的纯文本内容。

        :return: 纯文本内容。
        """
        return self.labelItem.toPlainText()

    def getHtml(self):
        """
        获取输入文本框的 HTML 内容。

        :return: HTML 内容。
        """
        return self.labelItem.toHtml()

    def setHtml(self, html):
        """
        设置输入文本框的 HTML 内容，准备几何变化并更新布局。

        :param html: 要设置的 HTML 内容。
        """
        self.prepareGeometryChange()
        self.labelItem.setHtml(html)
        self._font.setPointSize(6)
        self.labelItem.setFont(self._font)
        self.updateGeometry()
        self.update()

    def setTextColor(self, color):
        """
        设置输入文本框的默认文本颜色。

        :param color: 颜色对象。
        """
        self.labelItem.setDefaultTextColor(color)

    def mouseDoubleClickEvent(self, event):
        """
        处理鼠标双击事件，调用父类的鼠标双击事件处理方法。

        :param event: 鼠标双击事件对象。
        """
        super(NodeName, self).mouseDoubleClickEvent(event)

    def isRenamable(self):
        """
        判断节点是否可重命名，调用父节点的 isRenamable 方法。

        :return: 布尔值，表示节点是否可重命名。
        """
        return self.parentItem().isRenamable()

    def hoverEnterEvent(self, event):
        """
        处理悬停进入事件，标记为悬停并更新图形项。

        :param event: 悬停进入事件对象。
        """
        super(NodeName, self).hoverEnterEvent(event)
        self.hovered = True
        self.update()

    def hoverMoveEvent(self, event):
        """
        处理悬停移动事件，调用父节点的悬停移动事件处理方法。

        :param event: 悬停移动事件对象。
        """
        self.parentItem().hoverMoveEvent(event)

    def hoverLeaveEvent(self, event):
        """
        处理悬停离开事件，标记为未悬停并更新图形项。

        :param event: 悬停离开事件对象。
        """
        super(NodeName, self).hoverLeaveEvent(event)
        self.hovered = False
        self.update()

    def sizeHint(self, which, constraint):
        """
        获取节点名称的大小提示，根据字体和文本内容计算宽度和高度。

        :param which: 大小提示类型。
        :param constraint: 大小约束。
        :return: 大小提示对象。
        """
        # 计算文本的水平宽度
        w = QtGui.QFontMetrics(self.getFont()).horizontalAdvance(self.getPlainText())
        # 计算文本框的高度并加上 5
        h = self.labelItem.boundingRect().height() + 5
        return QtCore.QSizeF(w, h)

    def setGeometry(self, rect):
        """
        设置图形项的几何形状，准备几何变化，调用父类的设置几何形状方法并设置位置。

        :param rect: 矩形区域，用于设置图形项的位置和大小。
        """
        self.prepareGeometryChange()
        super(QGraphicsWidget, self).setGeometry(rect)
        self.setPos(rect.topLeft())
        self.labelItem.setGeometry(rect)


class UINodeBase(QGraphicsWidget, IPropertiesViewSupport, IUINode):
    """
    表示 UI 节点的基类，继承自 QGraphicsWidget、IPropertiesViewSupport 和 IUINode。

    提供节点的基本功能，包括节点的创建、布局、属性设置、事件处理等。
    """

    # 定义节点显示名称改变的信号，参数为字符串，表示新的显示名称
    displayNameChanged = QtCore.Signal(str)
    # 标记是否绘制标签
    drawlabel = None

    def __init__(
            self, raw_node, w=80, color=Colors.NodeBackgrounds, headColorOverride=None
    ):
        """
        初始化 UINodeBase 实例。

        :param raw_node: 原始节点对象。
        :param w: 节点的初始宽度，默认为 80。
        :param color: 节点的背景颜色，默认为 Colors.NodeBackgrounds。
        :param headColorOverride: 节点头部颜色的覆盖值，默认为 None。
        """
        super(UINodeBase, self).__init__()
        # 设置图形项的标志，包括可移动、可获得焦点、可选择和发送几何变化通知
        self.setFlag(QGraphicsWidget.ItemIsMovable)
        self.setFlag(QGraphicsWidget.ItemIsFocusable)
        self.setFlag(QGraphicsWidget.ItemIsSelectable)
        self.setFlag(QGraphicsWidget.ItemSendsGeometryChanges)
        # 设置缓存模式为设备坐标缓存
        self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
        # 设置焦点策略为强焦点
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        # 设置接受悬停事件
        self.setAcceptHoverEvents(True)
        # 设置节点的 Z 值为默认的层值
        self.setZValue(NodeDefaults().Z_LAYER)

        # 原始节点定义
        # 标记节点是否脏（需要更新）
        self.dirty = True
        # 标记节点是否正在计算
        self.computing = False
        self._rawNode = raw_node
        # 设置原始节点的包装器为当前节点
        self._rawNode.setWrapper(self)
        # 连接原始节点的各种信号到当前节点的相应方法
        self._rawNode.killed.connect(self.kill)
        self._rawNode.tick.connect(self.Tick)
        self._rawNode.errorOccurred.connect(self.onNodeErrorOccurred)
        self._rawNode.errorCleared.connect(self.onNodeErrorCleared)
        self._rawNode.setDirty.connect(self.setDirty)
        self._rawNode.computing.connect(self.setComputing)
        self._rawNode.computed.connect(self.setClean)

        # 自定义小部件数据
        self.custom_widget_data = {}
        # 心跳延迟时间
        self.heartBeatDelay = 0.5
        # 心跳时间间隔
        self.heartBeatTimeDelta = 0.0

        # 颜色和大小选项
        # 节点的基本颜色
        self.opt_node_base_color = Colors.NodeBackgrounds
        # 节点选中时的画笔颜色
        self.opt_selected_pen_color = Colors.NodeSelectedPenColor
        # 节点选中时的画笔类型
        self.optPenSelectedType = QtCore.Qt.SolidLine
        # 节点出错时的画笔类型
        self.optPenErrorType = QtCore.Qt.DashLine
        # 标记节点是否折叠
        self._collapsed = False
        # 左侧拉伸值
        self._left_stretch = 0
        self.color = color
        if self.drawlabel is None:
            self.drawlabel = True
        self.headColorOverride = headColorOverride
        # 节点头部的默认颜色
        self.headColor = NodeDefaults().PURE_NODE_HEAD_COLOR
        if raw_node.headerColor is not None:
            # 若原始节点有头部颜色，则覆盖默认颜色
            self.headColorOverride = QtGui.QColor.fromRgb(*raw_node.headerColor)
        self._w = 0
        self.h = 30
        # 节点的最小宽度
        self.minWidth = 50
        # 节点的最小高度
        self.minHeight = self.h
        # 节点标签的文本颜色
        self._labelTextColor = QtCore.Qt.white

        # 字体选项
        # 节点名称的字体
        self.nodeNameFont = QtGui.QFont("Consolas")
        self.nodeNameFont.setPointSize(6)

        # GUI 布局
        # 标记是否绘制布局调试信息
        self.drawLayoutsDebug = False
        # 创建一个垂直的图形线性布局，用于节点的整体布局
        self.nodeLayout = QGraphicsLinearLayout(QtCore.Qt.Vertical)
        # 设置节点布局的边距
        self.nodeLayout.setContentsMargins(
            NodeDefaults().CONTENT_MARGINS,
            NodeDefaults().CONTENT_MARGINS,
            NodeDefaults().CONTENT_MARGINS,
            NodeDefaults().CONTENT_MARGINS,
        )
        # 设置节点布局的间距
        self.nodeLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING)

        # 创建一个水平的图形线性布局，用于节点的头部布局
        self.headerLayout = QGraphicsLinearLayout(QtCore.Qt.Horizontal)

        # 创建一个节点名称小部件
        self.nodeNameWidget = NodeName(self)
        if self.drawlabel:
            # 若需要绘制标签，则将节点名称小部件添加到头部布局中
            self.headerLayout.addItem(self.nodeNameWidget)
        self.nodeNameWidget.setPos(0, 1)

        # 设置头部布局的边距
        self.headerLayout.setContentsMargins(0, 0, 0, 0)
        # 设置头部布局的间距
        self.headerLayout.setSpacing(3)

        # 设置头部布局的大小策略为扩展和最小
        self.headerLayout.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
        # 设置头部布局的最大高度为标签的高度
        self.headerLayout.setMaximumHeight(self.labelHeight)

        # 创建一个水平的图形线性布局，用于显示暴露的动作按钮
        self.exposedActionButtonsLayout = QGraphicsLinearLayout(QtCore.Qt.Horizontal)
        # 设置暴露的动作按钮布局的边距
        self.exposedActionButtonsLayout.setContentsMargins(0, 5, 0, 0)
        # 设置暴露的动作按钮布局的间距
        self.exposedActionButtonsLayout.setSpacing(2)
        # 设置暴露的动作按钮布局的大小策略为最大
        self.exposedActionButtonsLayout.setSizePolicy(
            QSizePolicy.Maximum, QSizePolicy.Maximum
        )
        # 将暴露的动作按钮布局添加到头部布局中
        self.headerLayout.addItem(self.exposedActionButtonsLayout)
        # 设置头部布局对暴露的动作按钮布局的对齐方式为右对齐
        self.headerLayout.setAlignment(
            self.exposedActionButtonsLayout, QtCore.Qt.AlignRight
        )

        # 创建一个垂直的图形线性布局，用于自定义布局
        self.customLayout = QGraphicsLinearLayout(QtCore.Qt.Vertical)
        # 设置自定义布局的边距
        self.customLayout.setContentsMargins(0, 0, 0, 0)
        # 设置自定义布局的间距
        self.customLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING)
        # 设置自定义布局的大小策略为首选和最小
        self.customLayout.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
        # 标记是否有自定义布局
        self.hasCustomLayout = False

        # 创建一个水平的图形线性布局，用于引脚布局
        self.pinsLayout = QGraphicsLinearLayout(QtCore.Qt.Horizontal)
        # 设置引脚布局的边距
        self.pinsLayout.setContentsMargins(0, 0, 0, 0)
        # 设置引脚布局的间距
        self.pinsLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING)
        # 设置引脚布局的大小策略为首选和最大
        self.pinsLayout.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)

        # 创建一个垂直的图形线性布局，用于输入引脚布局
        self.inputsLayout = QGraphicsLinearLayout(QtCore.Qt.Vertical)
        # 设置输入引脚布局的边距
        self.inputsLayout.setContentsMargins(0, 0, 0, 0)
        # 设置输入引脚布局的间距
        self.inputsLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING)
        # 设置输入引脚布局的大小策略为首选和最大
        self.inputsLayout.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)

        # 创建一个垂直的图形线性布局，用于输出引脚布局
        self.outputsLayout = QGraphicsLinearLayout(QtCore.Qt.Vertical)
        # 设置输出引脚布局的边距
        self.outputsLayout.setContentsMargins(0, 0, 0, 0)
        # 设置输出引脚布局的间距
        self.outputsLayout.setSpacing(NodeDefaults().LAYOUTS_SPACING)
        # 设置输出引脚布局的大小策略为首选和最大
        self.outputsLayout.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)

        # 将输入引脚布局和输出引脚布局添加到引脚布局中
        self.pinsLayout.addItem(self.inputsLayout)
        self.pinsLayout.addItem(self.outputsLayout)
        # 设置引脚布局对输入引脚布局的对齐方式为左对齐
        self.pinsLayout.setAlignment(self.inputsLayout, QtCore.Qt.AlignLeft)
        # 设置引脚布局对输出引脚布局的对齐方式为右对齐
        self.pinsLayout.setAlignment(self.outputsLayout, QtCore.Qt.AlignRight)
        # 设置引脚布局的首选宽度为节点布局的首选宽度
        self.pinsLayout.setPreferredWidth(self.nodeLayout.preferredWidth())

        # 将头部布局和引脚布局添加到节点布局中
        self.nodeLayout.addItem(self.headerLayout)
        # self.nodeLayout.addItem(self.customLayout)
        self.nodeLayout.addItem(self.pinsLayout)

        # 设置节点的布局为节点布局
        self.setLayout(self.nodeLayout)

        # 创建一个 SVG 图形项，用于显示节点图标
        self.svgIcon = QGraphicsSvgItem(self)
        self.svgIcon.setPos(-6, -6)

        # 节点的图像
        self._image = None
        # 节点画布的引用
        self.canvasRef = None
        # 节点的上下文菜单
        self._menu = QMenu()

        # 调整大小选项
        # 节点的初始矩形宽度
        self.initialRectWidth = self.minWidth
        # 节点的初始矩形高度
        self.initialRectHeight = self.minHeight
        # 标记节点是否可调整大小
        self.resizable = False
        # 标记节点是否正在调整大小
        self.bResize = False
        # 节点调整大小的方向
        self.resizeDirection = (0, 0)
        # 调整大小条的大小
        self.resizeStripsSize = 2
        # 调整大小条的状态数组
        self.resizeStrips = [
            0,
            0,
            0,
            0,  # Left, Top, Right, Bottom
            0,
            0,
            0,
            0,
        ]  # BottomRight, BottomLeft, TopLeft, TopRight
        # 节点的圆角因子
        self.roundness = NodeDefaults().CORNERS_ROUND_FACTOR

        # 隐藏/移动由组/折叠/由引脚控制
        # 按下的注释节点
        self.pressedCommentNode = None
        # 所属的注释节点
        self.owningCommentNode = None
        # 节点的矩形区域
        self._rect = QtCore.QRectF(0, 0, self.minWidth, self.minHeight)
        # 鼠标按下的位置
        self.mousePressPos = QtCore.QPointF()

        # 分组引脚
        # 输入分组引脚
        self.inputGroupPins = {}
        # 输出分组引脚
        self.outputGroupPins = {}

        # 动作按钮
        # 存储动作按钮的集合
        self._actionButtons = set()

        # 核心节点支持
        # 标记节点是否为临时节点
        self.isTemp = False
        # 标记节点是否为注释节点
        self.isCommentNode = False

        # 标记是否将输入暴露给复合节点
        self.bExposeInputsToCompound = False
        # 原始属性索引
        self.originalPropertyIndexes = {}
        # 编辑后的属性索引
        self.editedPropertyIndexes = {}

        # 折叠动作
        # 分组字典，包含输入和输出分组
        self._groups = {"input": {}, "output": {}}
        # 在菜单中添加一个 "ToggleCollapse" 动作
        self.actionToggleCollapse = self._menu.addAction("ToggleCollapse")
        # 设置动作的工具提示
        self.actionToggleCollapse.setToolTip("Toggles node's body collapsed or not")
        # 连接动作的触发信号到 toggleCollapsed 方法
        self.actionToggleCollapse.triggered.connect(self.toggleCollapsed)
        # 设置动作的数据为节点动作按钮信息
        self.actionToggleCollapse.setData(
            NodeActionButtonInfo(":/nodeCollapse.svg", CollapseNodeActionButton)
        )
        # 在菜单中添加一个 "Refresh" 动作
        self.actionRefresh = self._menu.addAction("Refresh")
        # 连接动作的触发信号到 onRefresh 方法
        self.actionRefresh.triggered.connect(self.onRefresh)
        # 在菜单中添加一个 "Expose properties" 动作
        self.actionToggleExposeWidgetsToCompound = self._menu.addAction(
            "Expose properties"
        )
        # 连接动作的触发信号到 onToggleExposeProperties 方法
        self.actionToggleExposeWidgetsToCompound.triggered.connect(
            self.onToggleExposeProperties
        )
        # 在菜单中添加一个 "Copy path" 动作
        self.actionCopyPath = self._menu.addAction("Copy path")
        # 连接动作的触发信号到 onCopyPathToClipboard 方法
        self.actionCopyPath.triggered.connect(self.onCopyPathToClipboard)
        # 连接原始节点的计算完成信号到 onComputed 方法
        self._rawNode.computed.connect(self.onComputed)

    def add_input_pin(self):
        # 示例：添加一个新的输入引脚
        new_pin_name = f"input_{len(self.UIinputs) + 1}"
        # 这里需要根据实际情况实现创建引脚的逻辑
        # 例如调用原始节点的创建输入引脚方法
        self._rawNode.createInputPin(new_pin_name, "ExecPin")
        # 更新 UI 显示
        self.refresh_pins()

    def add_output_pin(self):
        # 示例：添加一个新的输出引脚
        new_pin_name = f"output_{len(self.UIoutputs) + 1}"
        # 这里需要根据实际情况实现创建引脚的逻辑
        # 例如调用原始节点的创建输出引脚方法
        self._rawNode.createOutputPin(new_pin_name, "ExecPin")
        # 更新 UI 显示
        self.refresh_pins()

    def remove_input_pin(self):
        if self.UIinputs:
            last_input_pin = list(self.UIinputs.values())[-1]
            pin_name = last_input_pin.name
            # 调用原始节点的移除引脚方法
            self._rawNode.removePin(pin_name)
            # 更新 UI 显示
            self.refresh_pins()

    def remove_output_pin(self):
        if self.UIoutputs:
            last_output_pin = list(self.UIoutputs.values())[-1]
            pin_name = last_output_pin.name
            # 调用原始节点的移除引脚方法
            self._rawNode.removePin(pin_name)
            # 更新 UI 显示
            self.refresh_pins()

    def refresh_pins(self):
        # 重新创建节点的输入和输出引脚 UI
        self.create_input_pins()
        self.create_output_pins()
    def onRefresh(self):
        """
        刷新节点，调用原始节点的处理节点方法。
        """
        self._rawNode.processNode()

    def onCopyPathToClipboard(self):
        """
        将节点的路径复制到剪贴板。
        """
        QApplication.clipboard().clear()
        QApplication.clipboard().setText(self.path())

    def getLastErrorMessage(self):
        """
        获取节点的最后错误信息，调用原始节点的 getLastErrorMessage 方法。

        :return: 最后错误信息。
        """
        return self._rawNode.getLastErrorMessage()

    def hoverEnterEvent(self, event):
        """
        处理悬停进入事件，根据节点的有效性设置工具提示。

        :param event: 悬停进入事件对象。
        """
        super(UINodeBase, self).hoverEnterEvent(event)
        if not self.isValid():
            # 若节点无效，设置工具提示为最后错误信息
            self.setToolTip(self.getLastErrorMessage())
        else:
            # 若节点有效，设置工具提示为节点描述和计算时间
            self.setToolTip(
                "%s\nComputingTime: %s"
                % (rst2html(self.description()), self._rawNode._computingTime)
            )

    def eventDropOnCanvas(self):
        """
        处理节点在画布上的拖放事件，目前为空实现。
        """
        pass

    def setSingleLineName(self, bSingleLine=True):
        """
        设置节点名称是否为单行模式。

        :param bSingleLine: 是否为单行模式，默认为 True。
        """
        self.nodeNameWidget.labelItem.singleLine = bSingleLine

    def setNameValidationEnabled(self, bEnabled=True):
        """
        设置节点名称验证是否启用。

        :param bEnabled: 是否启用，默认为 True。
        """
        self.nodeNameWidget.labelItem.validator = (
            None if not bEnabled else NodeNameValidator()
        )

    def isNameValidationEnabled(self):
        """
        判断节点名称验证是否启用。

        :return: 布尔值，表示节点名称验证是否启用。
        """
        return self.nodeNameWidget.labelItem.validator is not None

    def onToggleExposeProperties(self):
        """
        切换是否将属性暴露给复合节点，保存编辑器状态。
        """
        self.setExposePropertiesToCompound(not self.bExposeInputsToCompound)
        EditorHistory().saveState(
            "{} exposing widgets".format(
                "Start" if self.bExposeInputsToCompound else "Stop"
            ),
            modify=True,
        )

    def setExposePropertiesToCompound(self, bExpose):
        """
        设置是否将属性暴露给复合节点，并更新节点。

        :param bExpose: 是否暴露，布尔值。
        """
        self.bExposeInputsToCompound = bExpose
        self.update()

    def __repr__(self):
        """
        返回节点的字符串表示形式，调用原始节点的 __repr__ 方法。

        :return: 节点的字符串表示形式。
        """
        return self._rawNode.__repr__()

    def __str__(self):
        """
        返回节点的字符串表示形式，调用原始节点的 __str__ 方法。

        :return: 节点的字符串表示形式。
        """
        return self._rawNode.__str__()

    @property
    def packageName(self):
        """
        获取节点所属的包名，调用原始节点的 packageName 属性。

        :return: 包名。
        """
        return self._rawNode.packageName

    @property
    def uid(self):
        """
        获取节点的唯一标识符，调用原始节点的 _uid 属性。

        :return: 唯一标识符。
        """
        return self._rawNode._uid

    @uid.setter
    def uid(self, value):
        """
        设置节点的唯一标识符，设置原始节点的 _uid 属性。

        :param value: 唯一标识符。
        """
        self._rawNode._uid = value

    @property
    def name(self):
        """
        获取节点的名称，调用原始节点的 name 属性。

        :return: 节点名称。
        """
        return self._rawNode.name

    @name.setter
    def name(self, value):
        """
        设置节点的名称，调用原始节点的 setName 方法。

        :param value: 节点名称。
        """
        self._rawNode.setName(value)

    @property
    def pins(self):
        """
        获取节点的所有引脚，调用原始节点的 pins 属性。

        :return: 引脚集合。
        """
        return self._rawNode.pins

    @property
    def orderedInputs(self):
        """
        获取节点的有序输入引脚，调用原始节点的 orderedInputs 属性。

        :return: 有序输入引脚集合。
        """
        return self._rawNode.orderedInputs

    @property
    def orderedOutputs(self):
        """
        获取节点的有序输出引脚，调用原始节点的 orderedOutputs 属性。

        :return: 有序输出引脚集合。
        """
        return self._rawNode.orderedOutputs

    @property
    def isCompoundNode(self):
        """
        判断节点是否为复合节点，调用原始节点的 isCompoundNode 属性。

        :return: 布尔值，表示节点是否为复合节点。
        """
        return self._rawNode.isCompoundNode

    def isValid(self):
        """
        判断节点是否有效，调用原始节点的 isValid 方法。

        :return: 布尔值，表示节点是否有效。
        """
        return self._rawNode.isValid()

    def setError(self, errorString):
        """
        设置节点的错误信息，调用原始节点的 setError 方法。

        :param errorString: 错误信息。
        """
        self._rawNode.setError(errorString)

    def clearError(self):
        """
        清除节点的错误信息，调用原始节点的 clearError 方法。
        """
        self._rawNode.clearError()

    def getName(self):
        """
        获取节点的名称，调用原始节点的 getName 方法。

        :return: 节点名称。
        """
        return self._rawNode.getName()

    def setName(self, name):
        """
        设置节点的名称，调用原始节点的 setName 方法。

        :param name: 节点名称。
        """
        self._rawNode.setName(name)

    def serialize(self):
        """
        序列化节点，调用原始节点的 serialize 方法。

        :return: 序列化后的节点数据。
        """
        return self._rawNode.serialize()

    def location(self):
        """
        获取节点的位置，调用原始节点的 location 方法。

        :return: 节点位置。
        """
        return self._rawNode.location()

    def path(self):
        """
        获取节点的路径，调用原始节点的 path 方法。

        :return: 节点路径。
        """
        return self._rawNode.path()

    def graph(self):
        """
        获取节点所属的图形，调用原始节点的 graph 方法。

        :return: 节点所属的图形对象。
        """
        return self._rawNode.graph()

    def isUnderActiveGraph(self):
        """
        判断节点是否在活动图形下，调用原始节点的 isUnderActiveGraph 方法。

        :return: 布尔值，表示节点是否在活动图形下。
        """
        return self._rawNode.isUnderActiveGraph()

    def autoAffectPins(self):
        """
        自动影响节点的引脚，调用原始节点的 autoAffectPins 方法。
        """
        self._rawNode.autoAffectPins()

    def isCallable(self):
        """
        判断节点是否可调用，调用原始节点的 isCallable 方法。

        :return: 布尔值，表示节点是否可调用。
        """
        return self._rawNode.isCallable()

    def category(self):
        """
        获取节点的类别，调用原始节点的 category 方法。

        :return: 节点类别。
        """
        return self._rawNode.category()

    def description(self):
        """
        获取节点的描述，调用原始节点的 description 方法。

        :return: 节点描述。
        """
        return self._rawNode.description()

    def call(self, name):
        """
        调用节点的指定方法，调用原始节点的 call 方法。

        :param name: 方法名称。
        """
        self._rawNode.call(name)

    @property
    def groups(self):
        """
        获取节点的分组字典。

        :return: 分组字典。
        """
        return self._groups

    @property
    def collapsed(self):
        """
        获取节点是否折叠的状态。

        :return: 布尔值，表示节点是否折叠。
        """
        return self._collapsed

    @collapsed.setter
    def collapsed(self, bCollapsed):
        """
        设置节点是否折叠的状态，处理节点折叠和展开的相关操作。

        :param bCollapsed: 是否折叠，布尔值。
        """
        if bCollapsed != self._collapsed:
            self._collapsed = bCollapsed
            # 调用 aboutToCollapse 方法，通知节点即将折叠或展开
            self.aboutToCollapse(self._collapsed)
            for i in range(0, self.inputsLayout.count()):
                # 隐藏或显示输入引脚布局中的项
                inp = self.inputsLayout.itemAt(i)
                inp.setVisible(not bCollapsed)
            for o in range(0, self.outputsLayout.count()):
                # 隐藏或显示输出引脚布局中的项
                out = self.outputsLayout.itemAt(o)
                out.setVisible(not bCollapsed)
            for cust in range(0, self.customLayout.count()):
                # 隐藏或显示自定义布局中的项
                out = self.customLayout.itemAt(cust)
                out.setVisible(not bCollapsed)
            # 更新节点的形状
            self.updateNodeShape()

    @property
    def image(self):
        """
        获取节点的图像。

        :return: 节点图像。
        """
        return self._image

    @image.setter
    def image(self, value):
        """
        设置节点的图像，加载 SVG 图像并设置元素 ID。

        :param value: 图像文件路径。
        """
        self._image = value
        self.svgIcon.renderer().load(value)
        elementName = QtCore.QFileInfo(value).baseName()
        self.svgIcon.setElementId(elementName)
        # self.svgIcon.setPos(self.geometry().topRight())

    @property
    def labelTextColor(self):
        """
        获取节点标签的文本颜色。

        :return: 颜色对象。
        """
        return self._labelTextColor

    @labelTextColor.setter
    def labelTextColor(self, value):
        """
        设置节点标签的文本颜色，并更新节点名称小部件的文本颜色。

        :param value: 颜色对象。
        """
        self._labelTextColor = value
        self.nodeNameWidget.setTextColor(self._labelTextColor)

    @property
    def UIPins(self):
        """
        获取节点的所有 UI 引脚。

        :return: 包含所有 UI 引脚的有序字典。
        """
        result = OrderedDict()
        for rawPin in self._rawNode.pins:
            uiPinRef = rawPin.getWrapper()
            if uiPinRef is not None:
                result[rawPin.uid] = uiPinRef()
        return result

    @property
    def UIinputs(self):
        """
        获取节点的所有 UI 输入引脚。

        :return: 包含所有 UI 输入引脚的有序字典。
        """
        result = OrderedDict()
        for rawPin in self._rawNode.orderedInputs.values():
            wrapper = rawPin.getWrapper()
            if wrapper is not None:
                result[rawPin.uid] = wrapper()
        return result

    @property
    def UIoutputs(self):
        """
        获取节点的所有 UI 输出引脚。

        :return: 包含所有 UI 输出引脚的有序字典。
        """
        result = OrderedDict()
        for rawPin in self._rawNode.orderedOutputs.values():
            wrapper = rawPin.getWrapper()
            if wrapper is not None:
                result[rawPin.uid] = wrapper()
        return result

    @property
    def namePinOutputsMap(self):
        """
        获取节点的输出引脚名称到 UI 引脚的映射。

        :return: 包含输出引脚名称到 UI 引脚映射的有序字典。
        """
        result = OrderedDict()
        for rawPin in self._rawNode.pins:
            if rawPin.direction == PinDirection.Output:
                wrapper = rawPin.getWrapper()
                if wrapper is not None:
                    result[rawPin.name] = wrapper()
        return result

    @property
    def namePinInputsMap(self):
        """
        获取节点的输入引脚名称到 UI 引脚的映射。

        :return: 包含输入引脚名称到 UI 引脚映射的有序字典。
        """
        result = OrderedDict()
        for rawPin in self._rawNode.pins:
            if rawPin.direction == PinDirection.Input:
                result[rawPin.name] = rawPin.getWrapper()()
        return result

    @property
    def w(self):
        """
        获取节点的宽度。

        :return: 节点宽度。
        """
        return self._w

    @w.setter
    def w(self, value):
        """
        设置节点的宽度。

        :param value: 节点宽度。
        """
        self._w = value

    @property
    def labelHeight(self):
        """
        获取节点标签的高度。

        :return: 节点标签的高度。
        """
        return self.nodeNameWidget.sizeHint(None, None).height()

    @property
    def labelWidth(self):
        """
        获取节点标签的宽度，包括按钮的宽度。

        :return: 节点标签的宽度。
        """
        headerWidth = self.nodeNameWidget.sizeHint(None, None).width()
        headerWidth += self.buttonsWidth()
        return max(headerWidth, self.minWidth)

    def getData(self, pinName):
        """
        获取指定名称引脚的数据。

        :param pinName: 引脚名称。
        :return: 引脚的数据，如果未找到则返回 None。
        """
        if pinName in [p.name for p in self.inputs.values()]:
            p = self.getPinSG(pinName, PinSelectionGroup.Inputs)
            return p.getData()

    def setData(self, pinName, data):
        """
        设置指定名称引脚的数据。

        :param pinName: 引脚名称。
        :param data: 要设置的数据。
        """
        if pinName in [p.name for p in self.outputs.values()]:
            p = self.getPinSG(pinName, PinSelectionGroup.Outputs)
            p.setData(data)

    def getPinSG(self, name, pinsGroup=PinSelectionGroup.BothSides):
        """
        根据名称和引脚组获取引脚的 UI 包装器。

        :param name: 引脚名称。
        :param pinsGroup: 引脚组，默认为 PinSelectionGroup.BothSides。
        :return: 引脚的 UI 包装器，如果未找到则返回 None。
        """
        pin = self._rawNode.getPinSG(name, pinsGroup)
        if pin is not None:
            if pin.getWrapper() is not None:
                return pin.getWrapper()()
        return None

    def isRenamable(self):
        """
        判断节点是否可重命名，默认返回 True。

        :return: 布尔值，表示节点是否可重命名。
        """
        return True

    def finalizeRename(self, accepted=False):
        """
        完成节点重命名操作，根据用户输入的名称更新节点名称并检查唯一性。

        :param accepted: 用户是否接受编辑，布尔值，默认为 False。
        """
        if accepted:
            name = self.nodeNameWidget.getPlainText()
            if self.isNameValidationEnabled():
                name = name.replace(" ", "")
            # 获取唯一的节点名称
            newName = self.canvasRef().graphManager.getUniqNodeName(name)
            self.setName(newName)
            self.setHeaderHtml(newName)
            # 请求填充属性视图
            self.canvasRef().requestFillProperties.emit(self.createPropertiesWidget)

    def onNodeErrorOccurred(self, *args, **kwargs):
        """
        处理节点发生错误的事件，更新节点的 UI 显示错误信息。

        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        # 改变节点 UI 为无效状态
        errorString = args[0]
        error = {"Node": self._rawNode.name, "Error": errorString}
        if ConfigManager().shouldRedirectOutput():
            errorLink = (
                    """<a href=%s><span style=" text-decoration: underline; color:red;">%s</span></a></p>"""
                    % (self._rawNode.name, error)
            )
            logging.error(errorLink)
        else:
            logging.error(errorString)
        self.setToolTip(errorString)
        self.update()

    def onNodeErrorCleared(self, *args, **kwargs):
        """
        处理节点错误清除的事件，恢复节点的 UI 显示正常信息。

        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        # 恢复节点 UI 为干净状态
        self.setToolTip(
            "%s\nComputingTime: %s"
            % (rst2html(self.description()), self._rawNode._computingTime)
        )
        self.update()

    def onComputed(self, *args, **kwargs):
        """
        处理节点计算完成的事件，更新节点的工具提示并刷新节点。

        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        self.setToolTip(
            "%s\nComputingTime: %s"
            % (rst2html(self.description()), self._rawNode._computingTime)
        )
        self.update()

    def toggleCollapsed(self):
        """
        切换节点的折叠状态。
        """
        self.collapsed = not self.collapsed

    def aboutToCollapse(self, futureCollapseState):
        """
        在节点即将折叠或展开时调用，目前为空实现。

        :param futureCollapseState: 节点未来的折叠状态，布尔值。
        """
        pass

    def setHeaderHtml(self, html):
        """
        设置节点头部的 HTML 内容。

        :param html: 要设置的 HTML 内容。
        """
        self.nodeNameWidget.setHtml(html)

    def getHeaderText(self):
        """
        获取节点头部的纯文本内容。

        :return: 节点头部的纯文本内容。
        """
        return self.nodeNameWidget.getPlainText()

    def getHeaderHtml(self):
        """
        获取节点头部的 HTML 内容。

        :return: 节点头部的 HTML 内容。
        """
        return self.nodeNameWidget.getHtml()

    def serializationHook(self):
        """
        序列化节点的 UI 特定信息。

        :return: 包含 UI 特定信息的字典。
        """
        # 此方法将由原始节点调用，以收集 UI 特定信息
        template = {}
        if self.resizable:
            template["resize"] = {"w": self._rect.right(), "h": self._rect.bottom()}
        template["collapsed"] = self.collapsed
        template["headerHtml"] = self.nodeNameWidget.getHtml()
        template["exposeInputsToCompound"] = self.bExposeInputsToCompound
        if len(self.groups) > 0:
            template["groups"] = {"input": {}, "output": {}}
            for name, grp in self.groups["input"].items():
                template["groups"]["input"][name] = grp.expanded
            for name, grp in self.groups["output"].items():
                template["groups"]["output"][name] = grp.expanded
        return template

    def buttonsWidth(self):
        """
        计算节点头部动作按钮的总宽度。

        :return: 动作按钮的总宽度。
        """
        # 动作按钮的宽度。10 是 SVG 图标的大小，可能需要将此值移到某些偏好设置中
        try:
            headerWidth = 0
            # 获取动作按钮的数量
            numActions = len(self._actionButtons)
            headerWidth += numActions * 10
            headerWidth += (
                    self.headerLayout.spacing() * 2 + NodeDefaults().CONTENT_MARGINS * 2
            )
            return headerWidth
        except:
            return 0

    def getNodeWidth(self, minimum=False):
        """
        获取节点的宽度。

        :param minimum: 是否获取最小宽度，布尔值，默认为 False。
        :return: 节点的宽度。
        """
        width = self.getPinsWidth() + self.pinsLayout.spacing() * 2
        if not minimum and self.resizable:
            width = max(self._rect.width(), width)
        width = max(width, self.labelWidth)
        return width

    def getNodeHeight(self, minimum=False):
        """
        获取节点的高度。

        :param minimum: 是否获取最小高度，布尔值，默认为 False。
        :return: 节点的高度。
        """
        h = self.nodeNameWidget.sizeHint(None, None).height()
        h += self.nodeLayout.spacing()
        try:
            # 获取输入和输出引脚的数量
            numInputs = len(self.UIinputs)
            numOutputs = len(self.UIoutputs)
            ipins = self.UIinputs.values()
            opins = self.UIoutputs.values()
            h += NodeDefaults().CONTENT_MARGINS * 2

            iPinsHeight = 0
            for pin in ipins:
                if pin.isVisible():
                    iPinsHeight += (
                            pin.sizeHint(None, None).height()
                            + NodeDefaults().LAYOUTS_SPACING
                    )
            oPinsHeight = 0
            for pin in opins:
                if pin.isVisible():
                    oPinsHeight += (
                            pin.sizeHint(None, None).height()
                            + NodeDefaults().LAYOUTS_SPACING
                    )

            h += max(iPinsHeight, oPinsHeight)

            igrhHeight = 0
            ogrhHeight = 0
            for grp in self.groups["input"].values():
                igrhHeight += (
                        grp.sizeHint(None, None).height() + NodeDefaults().LAYOUTS_SPACING
                )
            for grp in self.groups["output"].values():
                ogrhHeight += (
                        grp.sizeHint(None, None).height() + NodeDefaults().LAYOUTS_SPACING
                )
            h += max(igrhHeight, ogrhHeight)

        except Exception as e:
            print(e)
            pass
        custCount = 0
        for cust in range(0, self.customLayout.count()):
            out = self.customLayout.itemAt(cust)
            if out.isVisible():
                h += out.minimumHeight()
                custCount += 1
        if custCount > 0:
            h += self.customLayout.spacing() * self.customLayout.count()

        if h < self.minHeight:
            h = self.minHeight

        if not minimum and self.resizable:
            h = max(self._rect.height(), h)

        if self.collapsed:
            h = min(self.minHeight, self.labelHeight + self.nodeLayout.spacing() * 2)

        return h

    def getPinsWidth(self):
        """
        获取节点引脚的总宽度。

        :return: 节点引脚的总宽度。
        """
        iwidth = 0
        owidth = 0
        pinwidth = 0
        pinwidth2 = 0
        for i in self.UIPins.values():
            if i.direction == PinDirection.Input:
                iwidth = max(iwidth, i.sizeHint(None, None).width())
            else:
                owidth = max(owidth, i.sizeHint(None, None).width())
        for igrp in self.groups["input"].values():
            w = igrp.geometry().width()
            iwidth = max(iwidth, w)
        for ogrp in self.groups["output"].values():
            w = ogrp.geometry().width()
            owidth = max(owidth, w)
        return iwidth + owidth + pinwidth + pinwidth2 + Spacings.kPinOffset

    def setGeometry(self, rect):
        """
        设置节点的几何形状，准备几何变化，调用父类的设置几何形状方法并设置位置。

        :param rect: 矩形区域，用于设置节点的位置和大小。
        """
        self.prepareGeometryChange()
        super(QGraphicsWidget, self).setGeometry(rect)
        self.setPos(rect.topLeft())

    def itemChange(self, change, value):
        """
        处理节点的属性变化事件，根据变化类型更新节点的相关信息。

        :param change: 属性变化类型。
        :param value: 变化后的值。
        :return: 调用父类的 itemChange 方法的返回值。
        """
        if change == QGraphicsItem.ItemPositionChange:
            # 若节点位置发生变化，更新原始节点的位置
            self._rawNode.setPosition(value.x(), value.y())
        if change == QGraphicsItem.ItemVisibleChange:
            if self.owningCommentNode is not None:
                if self.owningCommentNode.collapsed:
                    self.onVisibilityChanged(False)
                else:
                    self.onVisibilityChanged(bool(value))
        if change == QGraphicsItem.ItemSelectedChange:
            if not value:
                # 若节点取消选中，清除节点名称小部件的焦点
                self.nodeNameWidget.labelItem.clearFocus()
        return super(UINodeBase, self).itemChange(change, value)

    def updateNodeShape(self):
        """
        更新节点的形状，包括无效化布局、更新几何形状和刷新节点。
        """
        self.prepareGeometryChange()
        # 无效化节点的所有布局
        self.invalidateNodeLayouts()
        self.updateGeometry()
        self.update()
        if self.canvasRef is not None:
            self.canvasRef().update()
        self.nodeNameWidget.updateGeometry()
        self.nodeNameWidget.update()
        # 设置引脚布局的首选宽度
        self.pinsLayout.setPreferredWidth(
            self.getNodeWidth() - self.nodeLayout.spacing()
        )
        # 设置头部布局的首选宽度
        self.headerLayout.setPreferredWidth(
            self.getNodeWidth() - self.nodeLayout.spacing()
        )
        # 设置自定义布局的首选宽度
        self.customLayout.setPreferredWidth(
            self.getNodeWidth() - self.nodeLayout.spacing()
        )

    def onVisibilityChanged(self, bVisible):
        """
        处理节点可见性变化的事件，目前为空实现。

        :param bVisible: 节点的可见性，布尔值。
        """
        pass

    def translate(self, x, y):
        """
        移动节点的位置，调用父类的 moveBy 方法。

        :param x: 水平移动的距离。
        :param y: 垂直移动的距离。
        """
        super(UINodeBase, self).moveBy(x, y)

    def sizeHint(self, which, constraint):
        """
        获取节点的大小提示，根据节点的宽度和高度返回大小提示对象。

        :param which: 大小提示类型。
        :param constraint: 大小约束。
        :return: 大小提示对象。
        """
        return QtCore.QSizeF(self.getNodeWidth(), self.getNodeHeight())

    def getImageDrawRect(self):
        """
        获取节点图标绘制的矩形区域。

        :return: 图标绘制的矩形区域。
        """
        topRight = self.boundingRect().topRight()
        topRight.setY(-12)
        topRight.setX(self.boundingRect().width() - 12)
        r = self.boundingRect()
        r.setWidth(24)
        r.setHeight(24)
        r.translate(topRight)
        return r

    def onChangeColor(self, label=False):
        """
        改变节点的颜色，弹出颜色选择对话框并更新节点颜色。

        :param label: 是否同时更新标签颜色，布尔值，默认为 False。
        """
        res = QColorDialog.getColor(self.color, None, "Node color setup")
        if res.isValid():
            res.setAlpha(80)
            self.color = res
            if label:
                self.update()

    def updateNodeHeaderColor(self):
        """
        更新节点头部的颜色，根据节点的计算状态和是否可调用设置头部颜色。
        """
        if self.computing:
            self.headColor = NodeDefaults().COMPUTING_NODE_HEAD_COLOR
        else:
            if self.headColorOverride is None:
                if self.isCallable():
                    self.headColor = NodeDefaults().CALLABLE_NODE_HEAD_COLOR
                else:
                    self.headColor = NodeDefaults().PURE_NODE_HEAD_COLOR
            else:
                self.headColor = self.headColorOverride

    def postCreate(self, jsonTemplate=None):
        """
        节点创建后的初始化操作，包括更新头部颜色、创建 UI 引脚包装器、更新节点形状等。

        :param jsonTemplate: 节点的 JSON 模板，默认为 None。
        """
        self.updateNodeHeaderColor()

        # 创建 UI 引脚包装器
        for i in self._rawNode.getOrderedPins():
            self._createUIPinWrapper(i)

        self.updateNodeShape()
        self.setPos(self._rawNode.x, self._rawNode.y)

        if self._rawNode.graph is None:
            print(self._rawNode.getName())
        assert self._rawNode.graph() is not None, "NODE GRAPH IS NONE"
        if self.canvasRef is not None:
            if self.canvasRef().graphManager.activeGraph() != self._rawNode.graph():
                self.hide()

        if not self.drawlabel:
            self.nodeNameWidget.hide()

        self.createActionButtons()

        headerHtml = self.name
        if jsonTemplate is not None and jsonTemplate["wrapper"] is not None:
            if "exposeInputsToCompound" in jsonTemplate["wrapper"]:
                self.setExposePropertiesToCompound(
                    jsonTemplate["wrapper"]["exposeInputsToCompound"]
                )
            if "collapsed" in jsonTemplate["wrapper"]:
                self.collapsed = jsonTemplate["wrapper"]["collapsed"]
            if "headerHtml" in jsonTemplate["wrapper"]:
                headerHtml = jsonTemplate["wrapper"]["headerHtml"]
            if "groups" in jsonTemplate["wrapper"]:
                try:
                    for groupName, expanded in jsonTemplate["wrapper"]["groups"][
                        "input"
                    ].items():
                        self.groups["input"][groupName].setExpanded(expanded)
                    for groupName, expanded in jsonTemplate["wrapper"]["groups"][
                        "output"
                    ].items():
                        self.groups["output"][groupName].setExpanded(expanded)
                except Exception as e:
                    pass

        description = self.description()
        if self.isDeprecated():
            description = self.deprecationMessage()
        if description:
            self.setToolTip(
                "%s\nComputingTime: %s"
                % (rst2html(self.description()), self._rawNode._computingTime)
            )
        else:
            self.setToolTip("\nComputingTime: %s" % self._rawNode._computingTime)
        if self.resizable:
            w = self.getNodeWidth()
            h = self.getNodeHeight()
            if jsonTemplate is not None:
                if "resize" in jsonTemplate["wrapper"]:
                    w = jsonTemplate["wrapper"]["resize"]["w"]
                    h = jsonTemplate["wrapper"]["resize"]["h"]
            self._rect.setWidth(w)
            self._rect.setHeight(h)
            self.updateNodeShape()

        self.setHeaderHtml(headerHtml)

    def getMetaData(self):
        """
        获取节点的元数据，调用原始节点的 getMetaData 方法。

        :return: 节点的元数据。
        """
        return self._rawNode.getMetaData()

    def createActionButtons(self):
        """
        创建节点的动作按钮，并将其添加到暴露的动作按钮布局中。
        """
        # 注意：指定了动作按钮类的动作将添加到节点名称旁边
        for action in self._menu.actions():
            actionData = action.data()
            if isinstance(actionData, NodeActionButtonInfo):
                actionButtonClass = actionData.actionButtonClass()
                svgFilePath = actionData.filePath()
                if actionButtonClass is None:
                    actionButtonClass = NodeActionButtonBase
                butt = actionButtonClass(svgFilePath, action, self)
                self.exposedActionButtonsLayout.insertItem(0, butt)
                self.exposedActionButtonsLayout.setAlignment(butt, QtCore.Qt.AlignRight)
                action.setVisible(False)

    def addWidget(self, widget):
        """
        向节点添加自定义小部件。

        :param widget: 要添加的小部件。
        """
        if not self.hasCustomLayout:
            self.nodeLayout.insertItem(2, self.customLayout)
            self.hasCustomLayout = True
        # 创建一个图形代理小部件
        ProxyWidget = QGraphicsProxyWidget()
        ProxyWidget.setWidget(widget)
        self.customLayout.addItem(ProxyWidget)

    def invalidateNodeLayouts(self):
        """
        无效化节点的所有布局，触发布局的重新计算。
        """
        self.inputsLayout.invalidate()
        self.outputsLayout.invalidate()
        self.pinsLayout.invalidate()
        self.headerLayout.invalidate()
        self.exposedActionButtonsLayout.invalidate()
        self.nodeLayout.invalidate()
        self.customLayout.invalidate()

    def isUnderCollapsedComment(self):
        """
        判断节点是否在折叠的注释节点下。

        :return: 布尔值，表示节点是否在折叠的注释节点下。
        """
        if self.owningCommentNode is None:
            return False
        else:
            if self.owningCommentNode.collapsed:
                return True

        parent = self.owningCommentNode.owningCommentNode
        while parent is not None:
            upperComment = parent
            if upperComment.collapsed:
                return True
            parent = upperComment.owningCommentNode
        return False

    def getTopMostOwningCollapsedComment(self):
        """
        获取节点所属的最顶层折叠注释节点。

        :return: 最顶层折叠注释节点，如果没有则返回 None。
        """
        """Returns top most owning comment. If bCollapsed=True, it will stop when first collapsed comment is found.
        """
        if self.owningCommentNode is None:
            return None
        # 构建注释节点的折叠状态链
        topMostComment = self.owningCommentNode
        parent = topMostComment.owningCommentNode

        chain = OrderedDict()
        chain[topMostComment] = topMostComment.collapsed

        while parent is not None:
            topMostComment = parent
            chain[topMostComment] = topMostComment.collapsed
            parent = topMostComment.owningCommentNode

        last = None
        for comment, collapsed in chain.items():
            if not comment.isVisible():
                continue
            if last is not None:
                if collapsed + last.collapsed == 1:
                    topMostComment = last
                    break
                last = comment
            else:
                last = comment

        return topMostComment

    def isDeprecated(self):
        """
        判断节点是否已弃用，调用原始节点的 isDeprecated 方法。

        :return: 布尔值，表示节点是否已弃用。
        """
        return self._rawNode.isDeprecated()

    def isExperimental(self):
        """
        判断节点是否为实验性节点，调用原始节点的 isExperimental 方法。

        :return: 布尔值，表示节点是否为实验性节点。
        """
        return self._rawNode.isExperimental()

    def deprecationMessage(self):
        """
        获取节点的弃用消息，调用原始节点的 deprecationMessage 方法。

        :return: 节点的弃用消息。
        """
        return self._rawNode.deprecationMessage()

    def updateOwningCommentNode(self):
        """
        更新节点所属的注释节点，根据碰撞检测结果确定所属的注释节点。
        """

        if self.owningCommentNode is not None and self.owningCommentNode.collapsed:
            return

        # 获取与节点碰撞的项
        collidingItems = self.collidingItems(QtCore.Qt.ContainsItemShape)
        collidingNodes = set()
        for item in collidingItems:
            if item.sceneBoundingRect().contains(
                    self.sceneBoundingRect()
            ) and isinstance(item, UINodeBase):
                if item.isCommentNode:
                    collidingNodes.add(item)
        owningCommentNode = None
        if len(collidingNodes) == 1:
            owningCommentNode = list(collidingNodes)[0]
        elif len(collidingNodes) > 1:
            # 找到最小的矩形
            smallest = list(collidingNodes)[0]
            for commentNode in collidingNodes:
                s1 = smallest.boundingRect().size()
                s2 = commentNode.boundingRect().size()
                if s1.width() > s2.width() and s1.height() > s2.height():
                    smallest = commentNode
                if self in commentNode.owningNodes:
                    commentNode.owningNodes.remove(self)
            owningCommentNode = smallest
        self.owningCommentNode = owningCommentNode
        if self.owningCommentNode is not None:
            if (
                    owningCommentNode._rawNode.graph()
                    == self.canvasRef().graphManager.activeGraph()
            ):
                self.owningCommentNode.owningNodes.add(self)

    def getCollidedNodes(self, bFullyCollided=True, classNameFilters=set()):
        """
        获取与节点碰撞的其他节点。

        :param bFullyCollided: 是否只考虑完全碰撞的节点，布尔值，默认为 True。
        :param classNameFilters: 类名过滤器，集合类型，默认为空集。
        :return: 与节点碰撞的其他节点的集合。
        """
        collidingItems = self.collidingItems()
        collidingNodes = set()
        for item in collidingItems:
            node = item.topLevelItem()
            if bFullyCollided:
                if self.sceneBoundingRect().contains(node.sceneBoundingRect()):
                    if node is not self and isinstance(node, UINodeBase):
                        if classNameFilters:
                            if node.__class__.__name__ not in classNameFilters:
                                continue
                        if (
                                node._rawNode.graph()
                                != self.canvasRef().graphManager.activeGraph()
                        ):
                            continue
                        collidingNodes.add(node)
            else:
                if node is not self and isinstance(node, UINodeBase):
                    if classNameFilters:
                        if node.__class__.__name__ not in classNameFilters:
                            continue
                    if (
                            node._rawNode.graph()
                            != self.canvasRef().graphManager.activeGraph()
                    ):
                        continue
                    collidingNodes.add(node)
        return collidingNodes

    def setDirty(self, *args, **kwargs):
        """
        设置节点为脏状态，更新节点的计算状态和头部颜色并刷新节点。

        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        self.computing = False
        self.dirty = True
        self.updateNodeHeaderColor()
        self.update()

    def setComputing(self, *args, **kwargs):
        """
        设置节点为计算状态，更新节点的计算状态和头部颜色并刷新节点。

        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        self.computing = True
        self.updateNodeHeaderColor()
        self.update()

    def setClean(self, *args, **kwargs):
        """
        设置节点为干净状态，更新节点的计算状态和头部颜色并刷新节点。

        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        self.computing = False
        self.dirty = False
        self.updateNodeHeaderColor()
        self.update()

    def paint(self, painter, option, widget):
        """
        绘制节点，调用默认的节点绘制方法，并根据需要绘制布局调试信息。

        :param painter: 绘图工具。
        :param option: 绘图选项。
        :param widget: 绘图所在的小部件。
        """
        NodePainter.default(self, painter, option, widget)
        if self.drawLayoutsDebug:
            painter.setPen(QtGui.QPen(QtCore.Qt.green, 0.75))
            painter.drawRect(self.headerLayout.geometry())
            painter.setPen(QtGui.QPen(QtCore.Qt.black, 0.75))
            painter.drawRect(self.nodeNameWidget.geometry())
            painter.drawRect(self.exposedActionButtonsLayout.geometry())
            painter.setPen(QtGui.QPen(QtCore.Qt.red, 0.75))
            painter.drawRect(self.pinsLayout.geometry())
            painter.setPen(QtGui.QPen(QtCore.Qt.green, 0.75))
            painter.drawRect(self.inputsLayout.geometry())
            painter.drawRect(self.outputsLayout.geometry())
            painter.setPen(QtGui.QPen(QtCore.Qt.blue, 0.75))
            painter.drawRect(self.customLayout.geometry())

    def shouldResize(self, cursorPos):
        """
        判断节点是否应该调整大小，并返回调整大小的方向。

        :param cursorPos: 鼠标光标位置。
        :return: 包含是否调整大小和调整大小方向的字典。
        """
        result = {"resize": False, "direction": self.resizeDirection}
        if self.resizeStrips[0] == 1:  # left
            result["resize"] = True
            result["direction"] = (-1, 0)
        if self.resizeStrips[1] == 1:  # top
            result["resize"] = True
            result["direction"] = (0, -1)
        if self.resizeStrips[2] == 1:  # right
            result["resize"] = True
            result["direction"] = (1, 0)
        if self.resizeStrips[3] == 1:  # bottom
            result["resize"] = True
            result["direction"] = (0, 1)
        if self.resizeStrips[4] == 1:  # bottom right
            result["resize"] = True
            result["direction"] = (1, 1)
        if self.resizeStrips[5] == 1:  # bottom left
            result["resize"] = True
            result["direction"] = (-1, 1)
        if self.resizeStrips[6] == 1:  # top left
            result["resize"] = True
            result["direction"] = (-1, -1)
        if self.resizeStrips[7] == 1:  # top right
            result["resize"] = True
            result["direction"] = (1, -1)
        return result

    def mousePressEvent(self, event):
        """
        处理鼠标按下事件，记录鼠标位置，更新节点状态，并根据需要开始调整大小。

        :param event: 鼠标按下事件对象。
        """
        self.update()
        self.mousePressPos = event.pos()
        self.pressedCommentNode = self.owningCommentNode
        super(UINodeBase, self).mousePressEvent(event)
        self.mousePressPos = event.scenePos()
        self.origPos = self.pos()
        self.initPos = self.pos()
        self.initialRect = self.boundingRect()
        if not self.collapsed and self.resizable:
            resizeOpts = self.shouldResize(self.mapToScene(event.pos()))
            if resizeOpts["resize"]:
                self.resizeDirection = resizeOpts["direction"]
                self.initialRectWidth = self.initialRect.width()
                self.initialRectHeight = self.initialRect.height()
                self.setFlag(QGraphicsItem.ItemIsMovable, False)
                self.bResize = True

    def mouseMoveEvent(self, event):
        """
        处理鼠标移动事件，根据节点是否正在调整大小执行相应的操作。

        :param event: 鼠标移动事件对象。
        """
        super(UINodeBase, self).mouseMoveEvent(event)
        # 调整大小
        if self.bResize:
            delta = event.scenePos() - self.mousePressPos
            if self.resizeDirection == (-1, 0):  # left
                posdelta = self.mapToScene(event.pos()) - self.origPos
                posdelta2 = self.mapToScene(event.pos()) - self.initPos
                newWidth = -posdelta2.x() + self.initialRectWidth
                if newWidth > self.getNodeWidth(minimum=True):
                    self.translate(posdelta.x(), 0)
                    self.origPos = self.pos()
                    self._rect.setWidth(newWidth)
                    self.updateNodeShape()
            elif self.resizeDirection == (0, -1):  # top
                posdelta = self.mapToScene(event.pos()) - self.origPos
                posdelta2 = self.mapToScene(event.pos()) - self.initPos
                minHeight = -posdelta2.y() + self.initialRectHeight
                if minHeight > self.getNodeHeight(minimum=True):
                    self.translate(0, posdelta.y())
                    self.origPos = self.pos()
                    self._rect.setHeight(minHeight)
                    self.updateNodeShape()
            elif self.resizeDirection == (1, 0):  # right
                newWidth = delta.x() + self.initialRectWidth
                if newWidth > self.getNodeWidth(minimum=True):
                    self._rect.setWidth(newWidth)
                    self.w = newWidth
                    self.updateNodeShape()
            elif self.resizeDirection == (0, 1):  # bottom
                newHeight = delta.y() + self.initialRectHeight
                if newHeight > self.getNodeHeight(minimum=True):
                    self._rect.setHeight(newHeight)
                    self.updateNodeShape()
            elif self.resizeDirection == (1, 1):  # bottom right
                newWidth = delta.x() + self.initialRectWidth
                newHeight = delta.y() + self.initialRectHeight
                if newWidth > self.getNodeWidth(minimum=True):
                    self._rect.setWidth(newWidth)
                    self.w = newWidth
                    self.updateNodeShape()
                if newHeight > self.getNodeHeight(minimum=True):
                    self._rect.setHeight(newHeight)
                    self.updateNodeShape()
            elif self.resizeDirection == (-1, 1):  # bottom left
                newHeight = delta.y() + self.initialRectHeight
                if newHeight > self.getNodeHeight(minimum=True):
                    self._rect.setHeight(newHeight)
                posdelta = self.mapToScene(event.pos()) - self.origPos
                posdelta2 = self.mapToScene(event.pos()) - self.initPos
                newWidth = -posdelta2.x() + self.initialRectWidth
                if newWidth > self.getNodeWidth(minimum=True):
                    self.translate(posdelta.x(), 0)
                    self.origPos = self.pos()
                    self._rect.setWidth(newWidth)
                self.updateNodeShape()
            elif self.resizeDirection == (-1, -1):  # top left
                posdelta = self.mapToScene(event.pos()) - self.origPos
                posdelta2 = self.mapToScene(event.pos()) - self.initPos
                minHeight = -posdelta2.y() + self.initialRectHeight
                if minHeight > self.getNodeHeight(minimum=True):
                    self.translate(0, posdelta.y())
                    self.origPos = self.pos()
                    self._rect.setHeight(minHeight)
                newWidth = -posdelta2.x() + self.initialRectWidth
                if newWidth > self.getNodeWidth(minimum=True):
                    self.translate(posdelta.x(), 0)
                    self.origPos = self.pos()
                    self._rect.setWidth(newWidth)
                self.updateNodeShape()
            elif self.resizeDirection == (1, -1):  # top right
                posdelta = self.mapToScene(event.pos()) - self.origPos
                posdelta2 = self.mapToScene(event.pos()) - self.initPos
                minHeight = -posdelta2.y() + self.initialRectHeight
                if minHeight > self.getNodeHeight(minimum=True):
                    self.translate(0, posdelta.y())
                    self.origPos = self.pos()
                    self._rect.setHeight(minHeight)
                newWidth = delta.x() + self.initialRectWidth
                if newWidth > self.getNodeWidth(minimum=True):
                    self._rect.setWidth(newWidth)
                    self.w = newWidth
                self.updateNodeShape()
            self.update()

    def mouseReleaseEvent(self, event):
        """
        处理鼠标释放事件，停止调整大小，重置调整大小条状态，更新节点状态。

        :param event: 鼠标释放事件对象。
        """
        self.bResize = False
        self.resetResizeStrips()
        self.update()
        self.updateOwningCommentNode()
        if self.owningCommentNode != self.pressedCommentNode:
            if self.pressedCommentNode is not None:
                if self in self.pressedCommentNode.owningNodes:
                    self.pressedCommentNode.owningNodes.remove(self)
        super(UINodeBase, self).mouseReleaseEvent(event)

    def hoverLeaveEvent(self, event):
        """
        处理悬停离开事件，重置调整大小条状态并刷新节点。

        :param event: 悬停离开事件对象。
        """
        self.resetResizeStrips()
        self.update()

    def hoverMoveEvent(self, event):
        """
        处理悬停移动事件，根据鼠标位置更新调整大小条状态并刷新节点。

        :param event: 悬停移动事件对象。
        """
        if self.resizable and not self.collapsed:
            height = self.geometry().height()
            width = self.geometry().width()
            rf = NodeDefaults().CORNERS_ROUND_FACTOR

            leftStrip = QtCore.QRectF(0, rf, self.resizeStripsSize, height - rf * 2)
            topStrip = QtCore.QRectF(rf, 0, width - rf * 2, self.resizeStripsSize)
            rightStrip = QtCore.QRectF(
                width - self.resizeStripsSize,
                rf,
                self.resizeStripsSize,
                height - rf * 2,
            )
            bottomStrip = QtCore.QRectF(
                rf,
                height - self.resizeStripsSize,
                width - rf * 2,
                self.resizeStripsSize,
            )

            bottomRightStrip = QtCore.QRectF(width - rf, height - rf, rf, rf)
            bottomLeftStrip = QtCore.QRectF(0, height - rf, rf, rf)
            topLeftStrip = QtCore.QRectF(0, 0, rf, rf)
            topRightStrip = QtCore.QRectF(width - rf, 0, rf, rf)

            # 检测鼠标在节点上的位置
            self.resizeStrips[0] = 1 if leftStrip.contains(event.pos()) else 0
            self.resizeStrips[1] = 1 if topStrip.contains(event.pos()) else 0
            self.resizeStrips[2] = 1 if rightStrip.contains(event.pos()) else 0
            self.resizeStrips[3] = 1 if bottomStrip.contains(event.pos()) else 0

            self.resizeStrips[4] = 1 if bottomRightStrip.contains(event.pos()) else 0
            self.resizeStrips[5] = 1 if bottomLeftStrip.contains(event.pos()) else 0
            self.resizeStrips[6] = 1 if topLeftStrip.contains(event.pos()) else 0
            self.resizeStrips[7] = 1 if topRightStrip.contains(event.pos()) else 0

            self.update()

    def contextMenuEvent(self, event):
        """
        处理上下文菜单事件，显示节点的上下文菜单。

        :param event: 上下文菜单事件对象。
        """
        self._menu.exec_(event.screenPos())

    def clone(self):
        """
        克隆节点，创建一个新的节点并复制原始节点的属性。

        :return: 克隆后的节点对象。
        """
        templ = self.serialize()
        templ["name"] = self.name
        templ["uuid"] = str(uuid.uuid4())
        for inp in templ["inputs"]:
            inp["uuid"] = str(uuid.uuid4())
        for out in templ["outputs"]:
            out["uuid"] = str(uuid.uuid4())
        new_node = self.canvasRef().createNode(templ)
        return new_node

    def createPropertiesWidget(self, propertiesWidget):
        """
        创建节点的属性视图小部件。

        :param propertiesWidget: 属性视图小部件。
        """
        # 创建一个名为 "Base" 的可折叠表单小部件
        baseCategory = CollapsibleFormWidget(headName="Base")

        # 创建一个只读的 QLineEdit 小部件，显示节点名称
        le_name = QLineEdit(self.getName())
        le_name.setReadOnly(True)
        baseCategory.addWidget("Name", le_name)

        # 创建一个只读的 QLineEdit 小部件，显示节点所属的图形名称
        leUid = QLineEdit(self._rawNode.graph().name)
        leUid.setReadOnly(True)
        baseCategory.addWidget("Owning graph", leUid)

        text = "{0}".format(self.packageName)
        if self._rawNode.lib:
            text += " | {0}".format(self._rawNode.lib)
        text += " | {0}".format(self._rawNode.__class__.__name__)
        # 创建一个只读的 QLineEdit 小部件，显示节点的类型信息
        leType = QLineEdit(text)
        leType.setReadOnly(True)
        baseCategory.addWidget("Type", leType)

        # 将 "Base" 可折叠表单小部件添加到属性视图小部件中
        propertiesWidget.addWidget(baseCategory)

        # 创建一个名为 "Inputs" 的可折叠表单小部件
        inputsCategory = CollapsibleFormWidget(headName="Inputs")
        self.createInputWidgets(inputsCategory)
        if inputsCategory.Layout.count() > 0:
            # 若 "Inputs" 可折叠表单小部件中有内容，则将其添加到属性视图小部件中
            propertiesWidget.addWidget(inputsCategory)

        # 创建一个名为 "Info" 的可折叠表单小部件，初始为折叠状态，隐藏标签
        Info = CollapsibleFormWidget(headName="Info", collapsed=True, hideLabels=True)
        # 创建一个 QTextBrowser 小部件，显示节点的描述信息
        doc = QTextBrowser()
        doc.setOpenExternalLinks(True)
        doc.setHtml(rst2html(self.description()))
        Info.addWidget(widget=doc)
        # 将 "Info" 可折叠表单小部件添加到属性视图小部件中
        propertiesWidget.addWidget(Info)

    def createInputWidgets(self, inputsCategory, inGroup=None, pins=True):
        """
        创建节点的输入引脚小部件。

        :param inputsCategory: 输入引脚小部件所属的可折叠表单小部件。
        :param inGroup: 输入引脚所属的分组，默认为 None。
        :param pins: 是否创建引脚小部件，布尔值，默认为 True。
        :return: 输入引脚小部件所属的可折叠表单小部件。
        """
        # 输入引脚
        if len([i for i in self.UIinputs.values()]) != 0:
            sortedInputs = self.UIinputs.values()
            for inp in sortedInputs:
                if inp.isArray() or inp.isDict() or inp._rawPin.hidden:
                    continue
                dataSetter = inp.call if inp.isExec() else inp.setData
                # 创建输入小部件
                w = createInputWidget(
                    inp.dataType,
                    dataSetter,
                    inp.defaultValue(),
                    inp.getInputWidgetVariant(),
                    pinAnnotations=inp._rawPin.annotationDescriptionDict,
                )
                if w:
                    w.setToolTip(inp.description)
                    # inp._rawPin.dataBeenSet.connect(w.setWidgetValueNoSignals)
                    w.blockWidgetSignals(True)
                    data = inp.currentData()
                    if isinstance(inp.currentData(), DictElement):
                        data = inp.currentData()[1]
                    w.setWidgetValue(data)
                    w.blockWidgetSignals(False)
                    w.setObjectName(inp.getFullName())
                    group = inGroup
                    if inGroup is None:
                        group = inp._rawPin.group
                    # 将输入小部件添加到可折叠表单小部件中
                    inputsCategory.addWidget(inp.name, w, group=group)
                    if inp.hasConnections():
                        w.setEnabled(False)
            return inputsCategory

    def createOutputWidgets(self, inputsCategory, headName="Outputs"):
        """
        创建节点的输出引脚小部件。

        :param inputsCategory: 输出引脚小部件所属的可折叠表单小部件。
        :param headName: 可折叠表单小部件的标题名称，默认为 "Outputs"。
        :return: 输出引脚小部件所属的可折叠表单小部件。
        """
        sortedInputs = sorted(self.UIPins.values(), key=lambda x: x.name)
        for inp in sortedInputs:
            if inp.isArray() or inp.isDict() or inp._rawPin.hidden:
                continue
            dataSetter = inp.call if inp.isExec() else inp.setData
            # 创建输入小部件
            w = createInputWidget(
                inp.dataType,
                dataSetter,
                inp.defaultValue(),
                inp.getInputWidgetVariant(),
                pinAnnotations=inp._rawPin.annotationDescriptionDict,
            )
            if w:
                w.setToolTip(inp.description)
                inp._rawPin.dataBeenSet.connect(w.setWidgetValueNoSignals)
                w.blockWidgetSignals(True)
                data = inp.currentData()
                if isinstance(inp.currentData(), DictElement):
                    data = inp.currentData()[1]
                w.setWidgetValue(data)
                w.blockWidgetSignals(False)
                w.setObjectName(inp.getFullName())
                # 将输出小部件添加到可折叠表单小部件中
                inputsCategory.addWidget(inp.name, w)
        return inputsCategory

    def getChainedNodes(self):
        """
        获取与节点链式连接的其他节点。

        :return: 与节点链式连接的其他节点的列表。
        """
        nodes = []
        for pin in self.UIinputs.values():
            for connection in pin.connections:
                node = connection.source().topLevelItem()  # topLevelItem
                nodes.append(node)
                nodes += node.getChainedNodes()
        return nodes

    def getBetwenLoopNodes(self, orig, bVisibleOnly=True):
        """
        获取在循环节点之间的其他节点。

        :param orig: 原始节点。
        :param bVisibleOnly: 是否只考虑可见的节点，布尔值，默认为 True。
        :return: 在循环节点之间的其他节点的列表。
        """
        nodes = []
        for pin in self.UIoutputs.values():
            for connection in pin.connections:
                node = connection.destination().topLevelItem()  # topLevelItem
                if node._rawNode.__class__.__name__ != "loopEnd":
                    if bVisibleOnly:
                        if node.isVisible():
                            nodes.append(node)
                    else:
                        nodes.append(node)
                    nodes += node.getBetwenLoopNodes(orig)
                else:
                    if node._rawNode.loopBeginNode.getData() != orig.path():
                        nodes.append(node)
                        nodes += node.getBetwenLoopNodes(orig)
        return nodes

    def collidesWithCommentNode(self):
        """
        判断节点是否与注释节点碰撞。

        :return: 若与注释节点碰撞则返回该注释节点，否则返回 None。
        """
        nodes = self.getCollidedNodes()
        result = None
        for n in nodes:
            if n.isCommentNode:
                result = n
                break
        return result

    def resetResizeStrips(self):
        """
        重置调整大小条的状态为 0。
        """
        for i in range(len(self.resizeStrips)):
            self.resizeStrips[i] = 0

    def kill(self, *args, **kwargs):
        """
        销毁节点，从场景中移除节点并删除节点对象。

        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        scene = self.scene()
        if scene is not None:
            self.scene().removeItem(self)
            del self

    def shoutDown(self):
        """
        关闭节点，目前为空实现。
        """
        pass

    def heartBeat(self):
        """
        节点的心跳函数，调用所有引脚的心跳函数。
        """
        for pin in self.UIPins.values():
            pin.heartBeat()

    def Tick(self, delta, *args, **kwargs):
        """
        节点的定时更新函数，处理 UI 相关的定时操作。

        :param delta: 时间间隔。
        :param args: 可变参数。
        :param kwargs: 关键字参数。
        """
        # 注意：不要在这里调用包装的原始节点的 Tick 方法！
        # 此 UI 节点的 Tick 方法由底层原始节点发出的信号调用
        # 在这里只处理 UI 相关的操作
        self.heartBeatTimeDelta += delta
        if self.heartBeatTimeDelta >= self.heartBeatDelay:
            self.heartBeat()
            self.heartBeatTimeDelta = 0.0

    def _createUIPinWrapper(self, rawPin, index=-1, group=None, linkedPin=None):
        """
        创建原始引脚的 UI 包装器，并将其添加到相应的布局中。

        :param rawPin: 原始引脚对象。
        :param index: 插入位置的索引，默认为 -1。
        :param group: 引脚所属的分组，默认为 None。
        :param linkedPin: 关联的引脚，默认为 None。
        :return: 原始引脚的 UI 包装器。
        """
        wrapper = rawPin.getWrapper()
        if wrapper is not None:
            return wrapper()

        p = getUIPinInstance(self, rawPin)
        p.call = rawPin.call

        grpItem = None
        if rawPin.group != "":
            if rawPin.direction == PinDirection.Input:
                if rawPin.group not in self.groups["input"]:
                    grpItem = PinGroup(self, rawPin.direction, rawPin.group)
                    self.inputsLayout.addItem(grpItem)
                elif rawPin.group in self.groups["input"]:
                    grpItem = self.groups["input"][rawPin.group]
            if rawPin.direction == PinDirection.Output:
                if rawPin.group not in self.groups["output"]:
                    grpItem = PinGroup(self, rawPin.direction, rawPin.group)
                    self.outputsLayout.addItem(grpItem)
                elif rawPin.group in self.groups["output"]:
                    grpItem = self.groups["output"][rawPin.group]

        name = rawPin.name
        lblName = name
        if rawPin.direction == PinDirection.Input:
            insertionIndex = -1
            if grpItem is not None:
                self.groups["input"][rawPin.group] = grpItem
                insertionIndex = (
                        findItemIndex(self.inputsLayout, grpItem) + grpItem.numPins() + 1
                )
                # 设置输入分组引脚布局的对齐方式为左对齐
                self.inputsLayout.setAlignment(grpItem, QtCore.Qt.AlignLeft)
                grpItem.addPin(p)
            # 将输入引脚插入到输入引脚布局中
            self.inputsLayout.insertItem(insertionIndex, p)
            # 设置输入引脚布局的对齐方式为左对齐
            self.inputsLayout.setAlignment(p, QtCore.Qt.AlignLeft)
            self.inputsLayout.invalidate()

        elif rawPin.direction == PinDirection.Output:
            insertionIndex = -1
            if grpItem is not None:
                self.groups["output"][rawPin.group] = grpItem
                insertionIndex = (
                        findItemIndex(self.outputsLayout, grpItem) + grpItem.numPins() + 1
                )
                # 设置输出分组引脚布局的对齐方式为右对齐
                self.outputsLayout.setAlignment(grpItem, QtCore.Qt.AlignRight)
                grpItem.addPin(p)
            # 将输出引脚插入到输出引脚布局中
            self.outputsLayout.insertItem(insertionIndex, p)
            # 设置输出引脚布局的对齐方式为右对齐
            self.outputsLayout.setAlignment(p, QtCore.Qt.AlignRight)
            self.outputsLayout.invalidate()

        # 设置引脚的大小策略为扩展
        p.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        self.update()
        self.updateNodeShape()
        p.syncDynamic()
        p.syncRenamable()
        if self.collapsed:
            p.hide()
        return p

    @staticmethod
    def removePinByName(node, name):
        """
        根据名称移除节点的引脚。

        :param node: 节点对象。
        :param name: 引脚名称。
        """
        pin = node.getPinSG(name)
        if pin:
            pin.kill()

    @staticmethod
    def recreate(node):
        templ = node.serialize()
        uid = node.uid
        node.kill()
        newNode = node.canvas.createNode(templ)
        newNode.uid = uid
        return newNode


def REGISTER_UI_NODE_FACTORY(packageName, factory):
    if packageName not in UI_NODES_FACTORIES:
        UI_NODES_FACTORIES[packageName] = factory


def getUINodeInstance(raw_instance):
    packageName = raw_instance.packageName
    instance = None
    if packageName in UI_NODES_FACTORIES:
        return UI_NODES_FACTORIES[packageName](raw_instance)
    else:
        return UINodeBase(raw_instance)
